//===============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Common Core Extensions
//===============================================================================

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Text;
using System.Xml;
using System.Reflection;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;

namespace EntLibContrib.Common.Configuration
{
	/// <summary>
	/// Defines a Polymorphic Configuration Element without having to be part of a PolymorphicConfigurationElementCollection.
	/// Works similar to what XmlSerializer does with xsi:type attribute.
	/// </summary>
	/// <remarks>The type attribute has to be the first one. If not specified, it is assumed the elements is of type T. 
	/// Implementation relies on private reflection as many methods are private on ConfigurationElement.
	/// </remarks>
	/// <typeparam name="T"></typeparam>
	public class TypeConfigurationElement<T> : AnonymousConfigurationElement
		where T : ConfigurationElement
	{
		#region Constants
		private const string typeAttribute = "type";
		#endregion

		#region Fields
		private AssemblyQualifiedTypeNameConverter typeConverter = new AssemblyQualifiedTypeNameConverter();
		private Type type;
		private T element;
		private bool isModified;
		#endregion

		#region Properties
		/// <summary>
		/// Type provided by the Deserialization.
		/// </summary>
		/// <value>The type.</value>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")]
		public virtual Type Type
		{
			get { return type; }
		}

		/// <summary>
		/// The Element itself within the TypeConfigurationElement container.
		/// </summary>
		/// <value>The element.</value>
		public T Element
		{
			get { return element; }
			set 
			{ 
				element = value;
        
				isModified = true;
			}
		}

		/// <summary>
		/// The Element type.
		/// </summary>
		/// <value>The type of the element.</value>
		public virtual Type ElementType
		{
			get
			{
				if (element == null)
				{
					return type;
				}
				else
				{
					Type elementType = element.GetType();

					return elementType == typeof(T) ? null : elementType;
				}
			}
		}

		/// <summary>
		/// Type used to create the Element during Deserialization.
		/// </summary>
		/// <value>The element type or default.</value>
		public virtual Type ElementTypeOrDefault
		{
			get { return ElementType == null ? typeof(T) : ElementType; }
		}

		/// <summary>
		/// Gets the name of the element type.
		/// </summary>
		/// <value>The name of the element type.</value>
		protected virtual string ElementTypeName
		{
			get { return ElementType == null ? null : typeConverter.ConvertToString(ElementType); }
		}
		#endregion

		#region Methods
		/// <summary>
		/// Indicates whether this configuration element has been modified since it was last saved or loaded, when implemented in a derived class.
		/// </summary>
		/// <returns>
		/// true if the element has been modified; otherwise, false.
		/// </returns>
		protected override bool IsModified()
		{
			return isModified || base.IsModified();
		}

		/// <summary>
		/// Deserializes the Element from the content of the reader.
		/// </summary>
		/// <param name="reader">The reader.</param>
		public virtual void Deserialize(XmlReader reader)
		{
			type = GetType(reader);

			element = CreateElement();

			DoDeserializeElement(reader);
		}

		/// <summary>
		/// Creates a new instance of the Element.
		/// </summary>
		/// <returns>The newly created instance.</returns>
		protected virtual T CreateElement()
		{
			object instance = Activator.CreateInstance(ElementTypeOrDefault);
      
			return instance as T;
		}

		/// <summary>
		/// Modifies the <see cref="T:System.Configuration.ConfigurationElement"/> object to remove all values that should not be saved.
		/// </summary>
		/// <param name="sourceElement">A <see cref="T:System.Configuration.ConfigurationElement"/> at the current level containing a merged view of the properties.</param>
		/// <param name="parentElement">The parent <see cref="T:System.Configuration.ConfigurationElement"/>, or null if this is the top level.</param>
		/// <param name="saveMode">A <see cref="T:System.Configuration.ConfigurationSaveMode"/> that determines which property values to include.</param>
		protected override void Unmerge(ConfigurationElement sourceElement, ConfigurationElement parentElement, ConfigurationSaveMode saveMode)
		{
			base.Unmerge(sourceElement, parentElement, saveMode);

			TypeConfigurationElement<T> castedSourceElement = sourceElement as TypeConfigurationElement<T>;

			Element = castedSourceElement.element;
		}

		/// <summary>
		/// Gets the type.
		/// </summary>
		/// <param name="reader">The reader.</param>
		/// <returns></returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "GetType")]
		protected virtual Type GetType(XmlReader reader)
		{
			Type elementType = null;

			if (reader.AttributeCount > 0)
			{
				reader.MoveToFirstAttribute();

				if (reader.Name == typeAttribute)
				{
					elementType = GetType(reader.Value);
				}
			}

			if (elementType == null)
			{
				reader.MoveToElement();
			}

			return elementType;
		}

		/// <summary>
		/// Gets the type.
		/// </summary>
		/// <param name="typeName">Name of the type.</param>
		/// <returns></returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "GetType")]
		protected virtual Type GetType(string typeName)
		{
			return (Type)typeConverter.ConvertFromString(typeName);
		}

		/// <summary>
		/// Writes the contents of this configuration element to the configuration file when implemented in a derived class.
		/// </summary>
		/// <param name="writer">The <see cref="T:System.Xml.XmlWriter"/> that writes to the configuration file.</param>
		/// <param name="serializeCollectionKey">true to serialize only the collection key properties; otherwise, false.</param>
		/// <returns>
		/// true if any data was actually serialized; otherwise, false.
		/// </returns>
		/// <exception cref="T:System.Configuration.ConfigurationErrorsException">
		/// The current attribute is locked at a higher configuration level.
		/// </exception>
		protected override bool SerializeElement(XmlWriter writer, bool serializeCollectionKey)
		{
			if (writer != null)
			{
				if (!string.IsNullOrEmpty(ElementTypeName))
				{
					writer.WriteAttributeString(typeAttribute, ElementTypeName);
				}

				DoSerializeElement(writer);
			}

			return true;
		}

		/// <summary>
		/// Does the serialize element.
		/// </summary>
		/// <param name="writer">The writer.</param>
		protected virtual void DoSerializeElement(XmlWriter writer)
		{
			object[] parameters = { writer, false };

			MethodInfo serializeElementMethod = typeof(ConfigurationElement).GetMethod("SerializeElement", BindingFlags.Instance | BindingFlags.NonPublic);

			serializeElementMethod.Invoke(Element, parameters);
		}

		/// <summary>
		/// Reads XML from the configuration file.
		/// </summary>
		/// <param name="reader">The <see cref="T:System.Xml.XmlReader"/> that reads from the configuration file.</param>
		/// <param name="serializeCollectionKey">true to serialize only the collection key properties; otherwise, false.</param>
		/// <exception cref="T:System.Configuration.ConfigurationErrorsException">
		/// The element to read is locked.
		/// - or -
		/// An attribute of the current node is not recognized.
		/// - or -
		/// The lock status of the current node cannot be determined.
		/// </exception>
		protected override void DeserializeElement(XmlReader reader, bool serializeCollectionKey)
		{
			Deserialize(reader);
		}

		/// <summary>
		/// Does the deserialize element.
		/// </summary>
		/// <param name="reader">The reader.</param>
		protected virtual void DoDeserializeElement(XmlReader reader)
		{
			object[] parameters = { reader, false };

			MethodInfo deserializeElementMethod = typeof(ConfigurationElement).GetMethod("DeserializeElement", BindingFlags.Instance | BindingFlags.NonPublic);

			deserializeElementMethod.Invoke(Element, parameters);
		}
		#endregion
	}
}
